Skip to content

Config-driven remote hosts for agentsview sync#600

Merged
wesm merged 7 commits into
mainfrom
issue-533
Jun 8, 2026
Merged

Config-driven remote hosts for agentsview sync#600
wesm merged 7 commits into
mainfrom
issue-533

Conversation

@wesm

@wesm wesm commented Jun 7, 2026

Copy link
Copy Markdown
Member

Closes #533.

Adds a [[remote_hosts]] array to config.toml so agentsview sync can sync
from a small fleet of machines without a per-host cron/launchd job.

[[remote_hosts]]
host = "devbox1"
user = "jesse"   # optional
port = 22        # optional (0/omitted = ssh default)

[[remote_hosts]]
host = "laptop2"

Behavior:

  • agentsview sync with no --host runs the local sync, then syncs each
    configured host over SSH in declared order. --full applies to every sync in
    the run, and an automatic data-version resync forces every configured remote
    full too (so remote sessions are re-parsed, not skipped).
  • agentsview sync --host X is unchanged: it ignores remote_hosts and syncs
    only that host, failing fast on error.
  • A configured host that fails at runtime is logged to the debug log and listed
    in a stderr summary; the remaining hosts still run, and the command exits
    non-zero if any configured host failed.
  • Invalid remote_hosts config fails before any sync: each entry needs a
    non-empty host, a port in 0..65535, and a host unique within the list (remote
    sync namespaces sessions and the skip cache by host, so duplicates would
    collide). This check runs only on the no---host path, so a bad entry cannot
    affect sync --host X. Host/user values are trimmed at load so the validated
    value matches what is passed to ssh.

Remote sync runs ssh non-interactively (BatchMode=yes plus a bounded
ConnectTimeout) so an unattended run fails fast instead of stalling on a
prompt or an unreachable host. It requires key-based (passwordless) SSH and
never prompts for a password; this applies to sync --host as well. Host-key
checking is left to the user's ssh config.

sync --help and the root help document the config block and the key-auth
requirement.

RemoteHosts is config-file/CLI only (json:"-"); it is not exposed to the
settings API, so there is no web-UI editing of the host list.

wesm and others added 4 commits June 7, 2026 08:13
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Jun 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (1121f9f)

Medium/High issues found; no Critical findings.

High

  • internal/config/config.go:182, cmd/agentsview/sync.go:118
    ValidateRemoteHosts accepts any non-empty host/user, and runRemoteSyncOnce passes those values into SSH. Because the SSH argument builder places the target before the -- delimiter, a configured host beginning with an SSH option can be interpreted as an option rather than a hostname. For example, host = "-oProxyCommand=sh -c '...'" could execute an attacker-controlled ProxyCommand locally during agentsview sync.

    Fix: Reject host and user values that can be parsed as SSH options, especially values beginning with - or containing control characters. Move the SSH option delimiter before the target, e.g. ssh [opts] -- target remote-command. Apply the same validation to config-driven hosts and the existing --host path.

Medium

  • cmd/agentsview/sync.go:120
    Configured remotes that share the same host but differ by user or port are synced under the same remote identity. RemoteSync uses Host alone for Machine, session ID prefixes, path rewrites, and remote skip-cache keys, so entries like user=a host=x and user=b host=x can overwrite or merge sessions and skip-cache state.

    Fix: Either reject duplicate remote_hosts entries with the same host in ValidateRemoteHosts, or introduce a distinct stable remote identity that includes user and port, then use it consistently for machine names, ID prefixes, and remote skipped-file cache keys.


Panel: ci_default_security | Synthesis: codex, 12s | Members: codex_default (codex/default, done, 2m52s), codex_security (codex/security, done, 2m34s) | Total: 5m38s

ssh.RemoteSync namespaces sessions (Machine, IDPrefix) and the remote
skip cache by host alone, so two remote_hosts entries sharing a host
collide regardless of user/port: they thrash each other's skip cache
each run and share the host~ session ID namespace. Reject duplicate
host values in ValidateRemoteHosts instead of silently sharing or
overwriting cached state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Jun 7, 2026

Copy link
Copy Markdown

roborev: Combined Review (10eaf8b)

Summary verdict: one medium issue remains; no high or critical findings were reported.

Medium

  • cmd/agentsview/sync.go:87 - Configured remote hosts only receive cfg.Full, while runLocalSync can trigger an automatic full resync when database.NeedsResync() is true. After a data-version bump, remote sessions preserved from the old DB can remain stale because remote sync loads the existing remote skip cache and skips unchanged remote files.

    Suggested fix: Capture didResync := runLocalSync(...), compute forceFull := cfg.Full || didResync, pass forceFull into runRemoteHosts, and add coverage confirming that an automatic local resync propagates full=true to configured remotes.


Panel: ci_default_security | Synthesis: codex, 7s | Members: codex_default (codex/default, done, 4m55s), codex_security (codex/security, done, 1m0s) | Total: 6m2s

runLocalSync returns whether a full resync ran (--full or an automatic
data-version resync). doSync discarded it and always passed cfg.Full to
the remote fan-out. Propagate it: a local resync now forces every
configured remote full too, so remote sessions are re-parsed rather than
skipped via the remote skip cache.

This is correct-by-construction rather than a live fix: a resync swaps in
a fresh DB whose remote_skipped_files table starts empty, so remotes
already re-process fully today. Propagating the signal keeps that correct
if the remote skip cache is ever preserved across a resync, and matches
how pg_watch/usage/main already branch on NeedsResync.

Extracts syncLocalAndRemotes as an injectable seam and tests that an
automatic resync propagates full=true to every configured host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Jun 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (1fe7ca8)

No issues found.


Panel: ci_default_security | Synthesis: codex | Members: codex_default (codex/default, done, 4m30s), codex_security (codex/security, done, 1m25s) | Total: 5m55s

Remote sync shells out to ssh with no options, so an unattended run
could stall on a password/passphrase or host-key prompt, or slow-fail
on an unreachable host -- and config-driven fan-out lets one bad host
drag down the rest. Pass BatchMode=yes (never prompt; require key-based
auth) and ConnectTimeout so runs fail fast with a clear error instead.
Defaults follow any caller sshOpts so an explicit override still wins.

Applies to both sync --host and the configured fan-out; host-key
checking is left to the user's ssh config (unchanged). Document the
key-based (passwordless) SSH requirement in sync and root help.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Jun 8, 2026

Copy link
Copy Markdown

roborev: Combined Review (dbcbb7f)

No issues found.


Panel: ci_default_security | Synthesis: codex | Members: codex_default (codex/default, done, 6m50s), codex_security (codex/security, done, 3m36s) | Total: 10m26s

@wesm wesm merged commit 4679648 into main Jun 8, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: config-driven list of remote hosts for sync

1 participant